using System;
using GammaJul.ReSharper.GammaXaml.Highlightings.CSharp;
using GammaJul.ReSharper.GammaXaml.Psi.CSharp;
using GammaJul.ReSharper.GammaXaml.Psi.Xaml;
using JetBrains.Annotations;
using JetBrains.ReSharper.Daemon.CSharp.Stages;
using JetBrains.ReSharper.Daemon.Stages;
using JetBrains.ReSharper.Daemon.Stages.Dispatcher;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.ReSharper.Psi.Xaml.Impl.Resolve;

namespace GammaJul.ReSharper.GammaXaml.Analysis.CSharp {

	/// <summary>
	/// Analyzer that checks that calls to DependencyProperty.Register/AddOwner/OverrideMetadata have:
	///  - An ownerType that derives from DependencyObject
	///  - A correct dependency property name for the field
	///  - An owner equals to the current type
	///  - A correct default value for the property type
	/// </summary>
	[ElementProblemAnalyzer(
		new[] { typeof(IInvocationExpression) },
		HighlightingTypes = new[] {
			typeof(OwnerMustBeDependencyObjectHighlighting),
			typeof(OwnerShouldBeContainingTypeHighlighting),
			typeof(MismatchedDpNameHighlighting),
			typeof(MismatchedDpDefaultValueTypeHighlighting)
		}
	)]
	public class DpRegistrationProblemAnalyzer : ElementProblemAnalyzer<IInvocationExpression> {

		protected override void Run(IInvocationExpression element, ElementProblemAnalyzerData data, IHighlightingConsumer consumer) {
			var method = element.InvocationExpressionReference.Resolve().DeclaredElement as IMethod;
			if (method == null)
				return;

			// checks that the invocation method is DependencyProperty.Something()
			IXamlTypeCache xamlTypeCache = XamlTypeCache.GetCache(element);
			IDeclaredType dpType = xamlTypeCache.DependencyProperty;
			if (!Equals(method.GetContainingType(), dpType.GetTypeElement()))
				return;

			ICSharpArgument ownerTypeArgument = null;
			ICSharpArgument nameArgument = null;
			ICSharpArgument typeMetadataArgument = null;
			switch (method.ShortName) {
				case "Register":
				case "RegisterReadOnly":
					nameArgument = element.GetArgumentByParameterName("name");
					ownerTypeArgument = element.GetArgumentByParameterName("ownerType");
					typeMetadataArgument = element.GetArgumentByParameterName("typeMetadata");
					break;
				case "AddOwner":
					ownerTypeArgument = element.GetArgumentByParameterName("ownerType");
					typeMetadataArgument = element.GetArgumentByParameterName("typeMetadata");
					break;
				case "OverrideMetadata":
					ownerTypeArgument = element.GetArgumentByParameterName("forType");
					typeMetadataArgument = element.GetArgumentByParameterName("typeMetadata");
					break;
				case "RegisterAttached":
				case "RegisterAttachedReadOnly":
					nameArgument = element.GetArgumentByParameterName("name");
					typeMetadataArgument = element.GetArgumentByParameterName("defaultMetadata");
					break;
			}

			IFieldDeclaration fieldDeclaration = GetDpFieldDeclaration(element);

			if (ownerTypeArgument != null)
				VerifyOwnerType(ownerTypeArgument, fieldDeclaration, element.GetContainingTypeDeclaration(), xamlTypeCache, consumer);

			if (nameArgument != null)
				VerifyName(nameArgument, fieldDeclaration, xamlTypeCache, consumer);

			if (typeMetadataArgument != null)
				VerifyDefaultValue(typeMetadataArgument, fieldDeclaration, xamlTypeCache, consumer);
		}

		[CanBeNull]
		private static IFieldDeclaration GetDpFieldDeclaration([NotNull] ITreeNode invocationNode) {
			var expressionInitializer = invocationNode.Parent as IExpressionInitializer;
			if (expressionInitializer == null)
				return null;

			return expressionInitializer.Parent as IFieldDeclaration;
		}

		/// <summary>
		/// Verify that an owner type argument derives from DependencyObject.
		/// </summary>
		/// <param name="ownerTypeArgument">The argument that represent a dependency property owner type.</param>
		/// <param name="fieldDeclaration">The dependency property field declaration.</param>
		/// <param name="containingTypeDecl">The containing type declaration.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <param name="consumer">The highlighting consumer.</param>
		private static void VerifyOwnerType([NotNull] IArgument ownerTypeArgument, [CanBeNull] IFieldDeclaration fieldDeclaration, [CanBeNull] ICSharpTypeDeclaration containingTypeDecl, [NotNull] IXamlTypeCache xamlTypeCache, [NotNull] IHighlightingConsumer consumer) {
			
			// we only handle typeof(x) expressions for verification
			var typeofExpr = ownerTypeArgument.Expression as ITypeofExpression;
			if (typeofExpr == null)
				return;

			// adds a warning if the owner isn't derived from DependencyObject
			IType ownerType = typeofExpr.ArgumentType;
			if (!ownerType.IsSubtypeOf(xamlTypeCache.DependencyObject)) {
				var highlighting = new OwnerMustBeDependencyObjectHighlighting(ownerType.GetClassType());
				consumer.AddHighlighting(highlighting, typeofExpr.TypeName.GetDocumentRange());
			}

			if (containingTypeDecl == null)
				return;

			// adds a warning if the owner isn't the current containing type (only if the current containing type derives from DependencyObject)
			ITypeElement containingTypeElement = containingTypeDecl.DeclaredElement;
			if (containingTypeElement != null
			&& containingTypeElement.IsDescendantOf(xamlTypeCache.DependencyObject.GetTypeElement())
			&& !Equals(containingTypeElement, ownerType.GetTypeElement<ITypeElement>())) {
				IField field = fieldDeclaration != null ? fieldDeclaration.DeclaredElement : null;
				consumer.AddHighlighting(new OwnerShouldBeContainingTypeHighlighting(typeofExpr, containingTypeElement, field));
			}
		}

		/// <summary>
		/// Verifies that a dependency property name matches its field name.
		/// </summary>
		/// <param name="nameArgument">The argument that represents a dependency property name.</param>
		/// <param name="fieldDeclaration">The dependency property field declaration.</param>
		/// <param name="xamlTypeCache">A <see cref="IXamlTypeCache"/> instance.</param>
		/// <param name="consumer">The highlighting consumer.</param>
		private static void VerifyName([NotNull] IArgument nameArgument, [CanBeNull] IFieldDeclaration fieldDeclaration, [NotNull] IXamlTypeCache xamlTypeCache, [NotNull] IHighlightingConsumer consumer) {
			if (fieldDeclaration == null)
				return;

			IConstantValueOwner constantValueOwner = nameArgument.Expression;
			if (constantValueOwner == null)
				return;

			ConstantValue constantValue = constantValueOwner.ConstantValue;
			if (!constantValue.IsString())
				return;

			var dpName = constantValue.Value as string;
			if (String.IsNullOrEmpty(dpName))
				return;

			string expectedFieldName;
			if (Equals(fieldDeclaration.Type, xamlTypeCache.DependencyProperty))
				expectedFieldName = dpName.ToDpFieldName();
			else if (Equals(fieldDeclaration.Type, xamlTypeCache.DependencyPropertyKey()))
				expectedFieldName = dpName.ToDpKeyFieldName();
			else
				expectedFieldName = null;

			if (expectedFieldName == null || expectedFieldName == fieldDeclaration.DeclaredElement.ShortName)
				return;

			IPsiSourceFile sourceFile = fieldDeclaration.GetSourceFile();
			if (sourceFile == null)
				return;

			// the field name and the dependency property name aren't matching, adds a warning
			var highlighting = new MismatchedDpNameHighlighting(fieldDeclaration.DeclaredElement, expectedFieldName, dpName, nameArgument, sourceFile);
			consumer.AddHighlighting(highlighting, nameArgument.Expression.GetDocumentRange());
			consumer.AddHighlighting(highlighting, fieldDeclaration.NameIdentifier.GetDocumentRange());
		}

		private static void VerifyDefaultValue([NotNull] IArgument typeMetadataArgument, [CanBeNull] IFieldDeclaration fieldDeclaration, [NotNull] IXamlTypeCache xamlTypeCache, [NotNull] IHighlightingConsumer consumer) {
			if (fieldDeclaration == null)
				return;

			// we only handle new X() expressions for verification
			var objectCreationExpr = typeMetadataArgument.Expression as IObjectCreationExpression;
			if (objectCreationExpr == null)
				return;

			// we only handle new X() expressions for verification, where X is derived from PropertyMetadata
			if (!objectCreationExpr.Type().IsSubtypeOf(xamlTypeCache.PropertyMetadata()))
				return;

			ICSharpArgument defaultValueArg = objectCreationExpr.GetArgumentByParameterName("defaultValue");
			if (defaultValueArg == null || defaultValueArg.Expression == null)
				return;

			IType defaultValueType = defaultValueArg.Expression.Type();
			if (defaultValueType.IsUnknown || defaultValueType.IsObject())
				return;

			IType dpRegisteredType = fieldDeclaration.GetDpOrDpKeyInitInfo(xamlTypeCache).RegisteredType;
			if (dpRegisteredType == null)
				return;

			// adds a warning if the default value type doesn't match the dependency property type
			if (!defaultValueType.IsSubtypeOf(dpRegisteredType) && !defaultValueType.IsSubtypeOf(dpRegisteredType.Unlift())) {
				consumer.AddHighlighting(new MismatchedDpDefaultValueTypeHighlighting(defaultValueArg, dpRegisteredType));
			}
		}

	}

}
